using System;
using System.ComponentModel.Composition;
using System.IO;
using System.Net;
using System.Text;

namespace FinMkts.Messaging
{
    /// <summary>
    /// Converts an <see cref="AddressedMessage"/> to and from a byte array
    /// </summary>
    /// <remarks>
    /// Messages are serialized as a sequence of bytes as follows:
    ///   topic length = 2 bytes,
    ///   body length = 4 bytes,
    ///   topic = UTF8 encoded string,
    ///   body = body.
    /// </remarks>
    [Export]
    public class AddressedMessageSerializer
    {
        private const int TopicLengthSize = 2;
        private const int BodyLengthSize = 4;
        public const int MinMessageSize = TopicLengthSize + BodyLengthSize;

        /// <summary>
        /// Converts an <see cref="AddressedMessage"/> into a byte array 
        /// </summary>
        /// <param name="message">The message to encode</param>
        /// <returns>Encoded version of the <paramref name="message"/></returns>
        public byte[] Serialize(AddressedMessage message)
        {
            if (message == null) throw new ArgumentNullException("message");
            if (string.IsNullOrEmpty(message.Topic)) throw new ArgumentNullException("message", "Topic property must be set");
            if (message.Body == null) throw new ArgumentNullException("message", "Body property must be set");
          
            var topicBytes = Encoding.UTF8.GetBytes(message.Topic);
            if (topicBytes.Length > short.MaxValue) throw new ArgumentNullException("message", "Topic length (when serialized) must be less than or equal to " + short.MaxValue);
            
            var buffer = new byte[TopicLengthSize + BodyLengthSize + topicBytes.Length + message.Body.Length];
            
            var offset = 0;
            Buffer.BlockCopy(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)topicBytes.Length)), 0, buffer, offset, TopicLengthSize);
            
            offset += TopicLengthSize;
            Buffer.BlockCopy(BitConverter.GetBytes(IPAddress.HostToNetworkOrder(message.Body.Length)), 0, buffer, offset, BodyLengthSize);
            
            offset += BodyLengthSize;
            Buffer.BlockCopy(topicBytes, 0, buffer, offset, topicBytes.Length);

            offset += topicBytes.Length;
            Buffer.BlockCopy(message.Body, 0, buffer, offset, message.Body.Length);
            return buffer;
        }

        /// <summary>
        /// Converts a serialized message back into a <see cref="AddressedMessage"/>
        /// </summary>
        /// <param name="buffer">Byte array containing the message at offset zero</param>
        /// <param name="offset">The index in <paramref name="buffer"/> to start deserializing from</param>
        /// <param name="count">The number of message bytes contained in the <paramref name="buffer"/></param>
        /// <returns>The decoded message</returns>
        public AddressedMessage Deserialize(byte[] buffer, ref int offset, int count)
        {
            if (buffer == null) throw new ArgumentNullException("buffer");
            if (offset >= count) return null;
            if (count < TopicLengthSize + BodyLengthSize) throw new ArgumentException("not enough bytes read to make a valid message");
            if (buffer.Length + offset < count) throw new ArgumentException("buffer is too small");

            int topicLength = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(buffer, offset));
            if (topicLength < 0) throw new ArgumentException("topic length is negative");
            offset += TopicLengthSize;

            int bodyLength = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(buffer, offset));
            if (bodyLength < 0) throw new ArgumentException("body length is negative");
            offset += BodyLengthSize;

            // there is no message to read
            if (topicLength + bodyLength == 0) return null;

            // read the topic there is enough bytes
            string topic = null;
            if (count - offset >= topicLength && topicLength > 0)
            {
                topic = Encoding.UTF8.GetString(buffer, offset, topicLength);
                offset += topicLength;
            }

            // throw exception if there is not enough bytes
            if (count - offset < bodyLength)
            {
                var message = string.Format("expected {0} bytes of body but only have {1} bytes{2}",
                    bodyLength, count - offset, topic == null ? "" : ", topic: " + topic);
                throw new ArgumentException(message);
            }

            // read the body
            var body = new byte[bodyLength];
            if (bodyLength > 0)
            {
                Buffer.BlockCopy(buffer, offset, body, 0, bodyLength);
                offset += bodyLength;
            }

            return new AddressedMessage {Topic = topic, Body = body};
        }
    }
}